package org.codefilarete.stalactite.engine;

import org.codefilarete.reflection.AccessorChain;
import org.codefilarete.reflection.Accessors;
import org.codefilarete.reflection.ValueAccessPoint;
import org.codefilarete.stalactite.query.model.ConditionalOperator;
import org.codefilarete.tool.collection.Arrays;
import org.danekja.java.util.function.serializable.SerializableBiConsumer;
import org.danekja.java.util.function.serializable.SerializableFunction;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * Contract that allows to create some query criteria based on property accessors
 *
 * @param <C> type of object returned by query execution
 */
public interface EntityCriteria<C, SELF extends EntityCriteria<C, SELF>> {
	
	/**
	 * Adds a criteria to the current instance with an {@code and} condition for the specified property
	 * and the given operator.
	 * 
	 * @param getter a method reference to the getter of the property to be evaluated
	 * @param operator operator of the criteria (will be the condition on the matching column)
	 * @return this instance for method chaining.
	 * @param <O> getter return type, also criteria value
	 * @throws IllegalArgumentException if the column matching the getter is not found.
	 */
	<O> SELF and(SerializableFunction<C, O> getter, ConditionalOperator<O, ?> operator);
	
	/**
	 * Adds a criteria to the current instance with an {@code and} condition for the specified property
	 * and the given operator.
	 * 
	 * @param setter a method reference to the setter of the property to be evaluated
	 * @param operator operator of the criteria (will be the condition on the matching column)
	 * @param <O> getter return type, also criteria value
	 * @return this
	 * @throws IllegalArgumentException if the column matching the setter was not found
	 */
	<O> SELF and(SerializableBiConsumer<C, O> setter, ConditionalOperator<O, ?> operator);
	
	/**
	 * Particular version of {@link #and(SerializableFunction, ConditionalOperator)} for an embedded or one-to-one bean
	 * property.
	 * 
	 * @param getter1 a method reference to the embedded bean
	 * @param getter2 a method reference to the getter of the embedded property to be evaluated.
	 * @param operator operator of the criteria (will be the condition on the matching column)
	 * @param <A> embedded bean type
	 * @param <B> embedded bean property type, also criteria value
	 * @return this
	 * @throws IllegalArgumentException if the column matching the getter was not found
	 */
	<A, B> SELF and(SerializableFunction<C, A> getter1, SerializableFunction<A, B> getter2, ConditionalOperator<B, ?> operator);
	
	/**
	 * Particular version of {@link #and(SerializableFunction, ConditionalOperator)} for a collection-of-element
	 * property to be able to target its component type with a {@link ConditionalOperator}. Without it, operators can
	 * only apply to the collection itself, which is rarely what is needed and not supported by Stalactite.
	 *
	 * @param collectionAccessor a method reference to the collection setter of the property to be evaluated
	 * @param operator operator of the criteria (will be the condition on the matching column)
	 * @param <O> getter return type, also criteria value
	 * @return this
	 * @throws IllegalArgumentException if the column matching the setter was not found
	 * @see #and(SerializableCollectionFunction, SerializableFunction, ConditionalOperator) for one-to-many case
	 */
	<S extends Collection<O>, O> SELF and(SerializableCollectionFunction<C, S, O> collectionAccessor, ConditionalOperator<O, ?> operator);
	
	/**
	 * Extension of {@link #and(SerializableFunction, ConditionalOperator)} for a collection property such as
	 * one-to-many to be able to target its component type with a {@link ConditionalOperator}. Without it, operators can
	 * only apply to the collection itself, which is rarely what is needed and not supported by Stalactite.
	 *
	 * @param collectionAccessor a method reference to the collection setter of the property to be evaluated
	 * @param elementAccessor    a method reference to the element setter of the property to be evaluated
	 * @param operator           operator of the criteria (will be the condition on the matching column)
	 * @param <O>                getter return type, also criteria value
	 * @return this
	 * @throws IllegalArgumentException if the column matching the setter was not found
	 * @see #and(SerializableCollectionFunction, ConditionalOperator) for collection-of-element case
	 */
	default <A, S extends Collection<O>, O> SELF and(SerializableCollectionFunction<C, S, O> collectionAccessor, SerializableFunction<O, A> elementAccessor, ConditionalOperator<A, ?> operator) {
		return and(new CriteriaPath<>(collectionAccessor).add(elementAccessor), operator);
	}
	
	/**
	 * Generic version of {@link #and(SerializableFunction, ConditionalOperator)} with a (long) path to a property 
	 * 
	 * @param propertyPath a path representing access to the property to be evaluated
	 * @param operator operator of the criteria (will be the condition on the matching column)
	 * @param <O> getter return type, also criteria value
	 * @return this
	 * @throws IllegalArgumentException if the column matching the propertyPath was not found
	 */
	<O> SELF and(CriteriaPath<C, O> propertyPath, ConditionalOperator<O, ?> operator);
	
	/**
	 * Adds a criteria to the current instance with an {@code or} condition for the specified property
	 * and the given operator.
	 *
	 * @param getter a method reference to the getter of the property to be evaluated
	 * @param operator operator of the criteria (will be the condition on the matching column)
	 * @param <O> getter return type, also criteria value
	 * @return this
	 * @throws IllegalArgumentException if the column matching the getter was not found
	 */
	<O> SELF or(SerializableFunction<C, O> getter, ConditionalOperator<O, ?> operator);
	
	/**
	 * Adds a criteria to the current instance with an {@code or} condition for the specified property
	 * and the given operator.
	 *
	 * @param setter a method reference to the setter of the property to be evaluated
	 * @param operator operator of the criteria (will be the condition on the matching column)
	 * @param <O> getter return type, also criteria value
	 * @return this
	 * @throws IllegalArgumentException if the column matching the setter was not found
	 */
	<O> SELF or(SerializableBiConsumer<C, O> setter, ConditionalOperator<O, ?> operator);
	
	/**
	 * Particular version of {@link #or(SerializableFunction, ConditionalOperator)} for a collection property to be
	 * able to target its component type with a {@link ConditionalOperator}. Without it, operators can only apply to
	 * the collection itself, which is rarely what is needed and not supported by Stalactite.
	 *
	 * @param collectionAccessor a method reference to the collection setter of the property to be evaluated
	 * @param operator operator of the criteria (will be the condition on the matching column)
	 * @param <O> getter return type, also criteria value
	 * @return this
	 * @throws IllegalArgumentException if the column matching the setter was not found
	 */
	<S extends Collection<O>, O> SELF or(SerializableCollectionFunction<C, S, O> collectionAccessor, ConditionalOperator<O, ?> operator);
	
	/**
	 * Generic version of {@link #or(SerializableFunction, ConditionalOperator)} with a (long) path to a property 
	 *
	 * @param propertyPath a path representing access to the property to be evaluated
	 * @param operator operator of the criteria (will be the condition on the matching column)
	 * @param <O> getter return type, also criteria value
	 * @return this
	 * @throws IllegalArgumentException if the column matching the propertyPath was not found
	 */
	<O> SELF or(CriteriaPath<C, O> propertyPath, ConditionalOperator<O, ?> operator);
	
	/**
	 * Starts a nested condition and returns it. At SQL rendering time, it should be embedded between parenthesis.
	 * Result must be used to fill the nested condition, not the current instance.
	 * After filling it, it must be closed by {@link #endNested()}
	 *
	 * @return a nested condition to be filled
	 */
	EntityCriteria<C, SELF> beginNested();
	
	/**
	 * Ends a nested condition started with {@link #beginNested()}. It returns the enclosing instance (the one on which {@link #beginNested()}
	 * was called).
	 *
	 * @return the enclosing condition
	 */
	EntityCriteria<C, SELF> endNested();
	
	default <O> SELF and(ValueAccessPoint<C> accessor, ConditionalOperator<O, ?> operator) {
		if (accessor instanceof AccessorChain) {
			return and(((AccessorChain<?, ?>) accessor).getAccessors(), operator);
		} else {
			return and(Arrays.asList(accessor), operator);
		}
	}
	
	default <O> SELF or(ValueAccessPoint<C> accessor, ConditionalOperator<O, ?> operator) {
		if (accessor instanceof AccessorChain) {
			return or(((AccessorChain<?, ?>) accessor).getAccessors(), operator);
		} else {
			return or(Arrays.asList(accessor), operator);
		}
	}
	
	<O> SELF and(List<? extends ValueAccessPoint<?>> accessors, ConditionalOperator<O, ?> operator);
	
	<O> SELF or(List<? extends ValueAccessPoint<?>> accessors, ConditionalOperator<O, ?> operator);
	
	interface OrderByChain<C, SELF extends OrderByChain<C, SELF>> {
		
		default SELF orderBy(SerializableFunction<C, ?> getter) {
			return orderBy(getter, Order.ASC);
		}
		
		default SELF orderBy(SerializableBiConsumer<C, ?> setter) {
			return orderBy(setter, Order.ASC);
		}
		
		default SELF orderBy(AccessorChain<C, ?> getter) {
			return orderBy(getter, Order.ASC);
		}
		
		SELF orderBy(SerializableFunction<C, ?> getter, Order order);
		
		SELF orderBy(SerializableBiConsumer<C, ?> setter, Order order);
		
		SELF orderBy(AccessorChain<C, ?> getter, Order order);
		
		SELF orderBy(AccessorChain<C, ?> getter, Order order, boolean ignoreCase);
		
		enum Order {
			ASC,
			DESC
		}
	}
	
	interface LimitAware<R> {
		
		R limit(int value);
		
		R limit(int value, Integer offset);
	}
	
	interface FluentOrderByClause<C, SELF extends FluentOrderByClause<C, SELF>> extends OrderByChain<C, SELF>, LimitAware<SELF> {
		
	}
	
	/**
	 * Represents a path of property to be given as a criteria.
	 * As a difference to {@link AccessorChain}, no particular behavior or feature is expected from it. It's only made
	 * to store the path of a property from a criteria point of view. Thus, {@link Collection} can be targetted and
	 * chained with one of its element property, not a {@link Collection} method.
	 * 
	 * @param <IN> the input type, representing the source object type
	 * @param <OUT> the output type, representing the target property type
	 */
	class CriteriaPath<IN, OUT> {
		
		private final List<ValueAccessPoint<?>> accessors = new ArrayList<>();
		
		public CriteriaPath(SerializableFunction<IN, OUT> accessor) {
			this.accessors.add(Accessors.accessorByMethodReference(accessor));
		}
		
		public CriteriaPath(SerializableBiConsumer<IN, OUT> accessor) {
			this.accessors.add(Accessors.mutatorByMethodReference(accessor));
		}
		
		public <S extends Collection<OUT>> CriteriaPath(SerializableCollectionFunction<IN, S, OUT> collectionAccessor) {
			this.accessors.add(Accessors.accessorByMethodReference(collectionAccessor));
		}
		
		public <S extends Collection<OUT>> CriteriaPath(SerializableCollectionBiConsumer<IN, S, OUT> collectionMutator) {
			this.accessors.add(Accessors.mutatorByMethodReference(collectionMutator));
		}
		
		public List<ValueAccessPoint<?>> getAccessors() {
			return accessors;
		}
		
		public <NEXT> CriteriaPath<IN, NEXT> add(SerializableFunction<OUT, NEXT> accessor) {
			this.accessors.add(Accessors.accessorByMethodReference(accessor));
			return (CriteriaPath<IN, NEXT>) this;
		}
		
		public <NEXT> CriteriaPath<IN, NEXT> add(SerializableBiConsumer<OUT, NEXT> accessor) {
			this.accessors.add(Accessors.mutatorByMethodReference(accessor));
			return (CriteriaPath<IN, NEXT>) this;
		}
		
		public <NEXT, S extends Collection<NEXT>> CriteriaPath<IN, NEXT> add(SerializableCollectionFunction<OUT, S, NEXT> accessor) {
			this.accessors.add(Accessors.accessorByMethodReference(accessor));
			return (CriteriaPath<IN, NEXT>) this;
		}
		
		public <NEXT, S extends Collection<NEXT>> CriteriaPath<IN, NEXT> add(SerializableCollectionBiConsumer<OUT, S, NEXT> accessor) {
			this.accessors.add(Accessors.mutatorByMethodReference(accessor));
			return (CriteriaPath<IN, NEXT>) this;
		}
	}
	
	/**
	 * Specialized version of {@link SerializableFunction} for collection accessors: the output type is the collection
	 * element type, which makes this not really a {@link SerializableFunction} but useful in the context of {@link CriteriaPath} 
	 * 
	 * @param <T> the input type
	 * @param <S> the collection type
	 * @param <O> the collection element type
	 * @author Guillaume Mary
	 */
	@FunctionalInterface
	interface SerializableCollectionFunction<T, S extends Collection<O>, O> extends SerializableFunction<T, S> {
		
	}
	
	@FunctionalInterface
	interface SerializableCollectionBiConsumer<T, S extends Collection<O>, O> extends SerializableBiConsumer<T, S> {
		
	}
}
